package com.hmdp.service.impl;
import java.time.LocalDateTime;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.redis.CacheClient;
import com.hmdp.utils.RedisConstants;
import com.hmdp.redis.RedisData;
import com.hmdp.utils.SystemConstants;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.domain.geo.GeoReference;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.CACHE_SHOP_KEY;
import static com.hmdp.utils.RedisConstants.SHOP_GEO_KEY;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    private  final StringRedisTemplate stringRedisTemplate;
    private  final CacheClient cacheClient;

    /**
     * 查询商品使用redis缓存
     *
     * @param id
     * @return
     */
    @Override
    public Result queryById(Long id) {
        //缓存穿透
        //Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, this::getById, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        Shop shop = queryWithLogicalExpire(id);
        log.debug("数据{}",shop);
        return Result.ok(shop);
    }
    //创建一个线程池
    private static final ExecutorService threadPool= Executors.newFixedThreadPool(10);

    /**
     * 通过逻辑过期时间解决缓存击穿问题
     * @param id
     * @return
     */
    public Shop queryWithLogicalExpire(Long id) {
        //1.查询是否命中缓存
        String key = CACHE_SHOP_KEY + id;
        String cache = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isBlank(cache)) {
            //2.缓存没有命中
            System.out.println("缓存没有命中");
            return null;
        }
        //3.命中缓存.进行反序列化
        RedisData redisData = JSONUtil.toBean(cache, RedisData.class);
        JSONObject data = (JSONObject) redisData.getData();
        Shop shopCache = JSONUtil.toBean(data, Shop.class);
        //3.1判断是否逻辑过期
        //4.未过期
        if (redisData.getExpireTime().isAfter(LocalDateTime.now())) {
            System.out.println("时间没有过期"+redisData.getExpireTime()+"现在"+LocalDateTime.now());
            return shopCache;
        }
        //5.已经过期
        //6.获取锁
        System.out.println("已经过期");
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        boolean lock = tryLock(lockKey);
        //7.获取锁成功
        if (lock) {
            System.out.println("获取锁成功开启线程");
            //7.1开启一个线程缓存的重建
            threadPool.submit(()->{
                try {
                    this.saveShop(id,20L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    unLock(lockKey);
                }
            });
        }
        //8.获取失败返回久的数据
        return  shopCache;

    }
    /**
     * 互斥锁解决缓存击穿
     * @param id
     * @return
     */
    public Result queryWithMutex(Long id) {
        //1.查询是否命中缓存
        String key = CACHE_SHOP_KEY + id;
        String cache = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(cache)) {
            //2.缓存命中
            return Result.ok(cache);
        }
        //3.未命中缓存
        if (cache != null) {
            return  null;
        }
        //4.获取锁
        boolean isLock = tryLock("lock:shop:" + id);
        Shop shop = null;
        try {
            //5.获取锁失败
            if (!isLock ) {
                Thread.sleep(50);
                //5.1休眠后继续尝试获取
                queryWithMutex(id);
            }
            //5.2获取锁成功
            //6.查询数据库
            shop = getById(id);
            if (shop == null) {
                //6.1缓存null值解决缓存击穿
                stringRedisTemplate.opsForValue().set(key,"",2,TimeUnit.MINUTES);
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            unLock("lock:shop:" + id);
        }


        return Result.ok(shop);
    }

    /**
     * 获取锁,给redis中存入setnx
     * @param key
     * @return
     */
    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);

        return BooleanUtil.isTrue(flag);
    }

    /**
     * 释放锁
     * @param key
     */
    private void  unLock(String key){
        stringRedisTemplate.delete(key);
    }

    //保存Redisdata对象
    public RedisData  saveShop(Long id,Long time){
        //查询
        Shop shop = getById(id);
        RedisData redisData = new RedisData();
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(time));
        redisData.setData(shop);
        String jsonShop = JSONUtil.toJsonStr(redisData);
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,jsonShop,time,TimeUnit.MINUTES);
        return redisData;
    }

    @Override
    @Transactional
    public Result update(Shop shop) {
        //1.先更新
        updateById(shop);
        if (shop.getId() == null) {
            Result.fail("id不能为空");
        }
        //2.删除缓存保证一致性
        stringRedisTemplate.delete(CACHE_SHOP_KEY+shop.getId());
        return Result.ok();
    }

    @Override
    public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
        //1.从数据库中查询
        if (x == null || y==null) {
            log.debug("从数据库中查询商品地址");
            Page<Shop> page = query().eq("type_id", typeId)
                    .page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
            return Result.ok(page.getRecords());
        }
        //2.计算分页
        int from=(current-1) *SystemConstants.DEFAULT_PAGE_SIZE;
        int end=current *SystemConstants.DEFAULT_PAGE_SIZE;
        //3.查询redis,按照距离排序,分页.结果:shopId,distance
        String key =SHOP_GEO_KEY+typeId;
        GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo()
                //GEOSEARCH key BYLONLAT X Y BYRADIUS 10 WITHDISTANCE
                .search(
                        key,//redis的key
                        GeoReference.fromCoordinate(new Point(x,y)), //传入的经纬度
                        new Distance(50000),//范围距离
                        RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end)//分页
                );
        //4.解析出id
        if (results == null) {
            return Result.ok(Collections.emptyList());
        }
        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();
        if (list.size()<= from) {
            return Result.ok(Collections.emptyList());
        }
        //4.1.截取from  - end 的部分
        List<Long> ids=new ArrayList<>(list.size());
        Map<String,Distance> distanceMap =new HashMap<>(list.size());
        list.stream().skip(from).forEach(r->{
            //4.2.获取店铺的id
            String shopIdStr = r.getContent().getName();
            ids.add(Long.valueOf(shopIdStr));
            //4.3.获取距离
            Distance distance = r.getDistance();
            distanceMap.put(shopIdStr,distance);
        });
        //5.根据id查询shop
        String idsStr = StrUtil.join(",",ids);
        log.debug("查询到的shop值{}",idsStr);
        List<Shop> shopList = query().in("id", ids).last("ORDER BY FIELD(id," + idsStr + ")").list();
        for (Shop shop : shopList) {
            Distance distance = distanceMap.get(shop.getId().toString());
            shop.setDistance(distance.getValue());
        }
        return Result.ok(shopList);
    }
}
